/* Copyright 2007 Sergejs Melderis (sergey.melderis@gmail.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
*/

package com.sm.binder;


import java.util.*;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.sql.*;
import java.math.BigDecimal;
import static com.sm.binder.DebugMessage.*;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * {@code ResultSetBinder} converts {@code ResultSet} rows
 * into objects, populated with the values from the columns of  {@code ResultSet}.
 *
 * {@code ResultSetBinder} uses {@link NameMatcher @NameMatcher} to match columns of
 * a {@code ResultSet} with method names. {@link NameMatcher  @NameMatcher} can be set
 * globally for all instances of {@code ResultSetBinder} using static method  {@link ResultSetBinder#setDefaultMatcher(NameMatcher)}.
 * To set {@code NameMatcher} per instance use  {@link ResultSetBinder#setNameMatcher(NameMatcher)}.
 *
 *
 * If no args constructors are available {@code ResultSetBinder} will try to populate objects passing
 * values from {@code ResultSet} to constructors.
 * Consider the following example class:
 * <pre>
 * public class Foo {
 *     private int foo;
 *     private float foo2;
 *     private String bar;
 *
 *     public Foo() {}
 *     public Foo(int foo, int foo2) { this.foo = foo; this.foo2 = foo2; }
 *
 *     public void setFoo(int foo) { this.foo = foo; }
 *     public void setBar(String bar) { this.bar = bar; }
 *     public void setFoo2(int foo2) { this.foo2 = foo2; }
 * }
 * </pre>
 * If {@code ResultSetBinder} matches {@code ResultSet} columns to {@code setFoo}, {@code setBar} and {@code setFoo2}
 * it will use constructor {@code Foo(int foo, int foo2)} and {@code setBar} to construct and
 * populate this object. Binding to constructors is not possible if arguments are  manipulated withing
 * the constructor body before setting the fields or if the field names are not the same as setter names
 * without "set".
 * <p>
 * Modified class Foo
 * <pre>
 * public class Foo {
 *     private int _foo;
 *     private float foo2;
 *     private String bar;
 *
 *     public Foo() {}
 *     public Foo(int foo, int foo2) { _foo = foo; this.foo2 = foo2 / 2; }
 *
 *     public void setFoo(int foo) { _foo = foo; }
 *     public void setBar(String bar) { this.bar = bar; }
 *     public void setFoo2(int foo2) { this.foo2 = foo2 / 2; }
 * }
 * </pre>
 * In this case ResultSetBinder can only bind to setXXX methods of Foo.
 *
 * @author Sergejs Melderis (sergey.melderis@gmail.com)
 */
public class ResultSetBinder<T> {
    private final Log log;

    private  static NameMatcher defaultMatcher = new DefaultNameMatcher();

    private NameMatcher nameMatcher = defaultMatcher;

    private final Class<T> type;

    private InstanceCreator<T> creator;

    /* List of column mappers */
    private final List<ColumnBinder> columnBinders = new ArrayList<ColumnBinder>();

    /* Nested result set binders */
    private final List<ResultSetBinder<?>> resultSetBinders = new ArrayList<ResultSetBinder<?>>();

    private int lastListSize = 10;

    /** Set to hold mapped column indices */
    private final Set<Integer> mappedColIndeces = new TreeSet<Integer>();

    private List<DebugMessage> debugMessages;
    private List<DebugMessage> warnMessages;

    private boolean debug;
    private boolean warn;


    /** Indicates that this the root or parent of all nested binders. */
    private boolean root = true;

    /**
     * Map from a method in T to a ResultSetBinder, the result of which is a parameter
     * for a method.
     */
    private final Map<Method, ResultSetBinder<?>> nestedBindersMap =
            new HashMap<Method, ResultSetBinder<?>>();

    /**
     * Creates new ResultSetBinder of type {@code type} with optional nested binders
     * {@code binders}.
     * 
     * @param type - the parameterized type of this binder
     * @param binders - optional nested binders.
     */
    public ResultSetBinder(Class<T> type, ResultSetBinder<?>... binders) {
        this.type = type;
        
        log = LogFactory.getLog(ResultSetBinder.class.getName() + "<" + type.getSimpleName() + ">");
        debug = log.isDebugEnabled();
        warn = log.isWarnEnabled();

        for (ResultSetBinder binder : binders) {
            resultSetBinders.add(binder);
            binder.root = false;
        }

        if (warn) {
            warnMessages = new ArrayList<DebugMessage>();
        }

        if (debug) {
            debugMessages = new ArrayList<DebugMessage>();
        }


    }

    /**
     * Behaves the same as the above constructor, but uses {@code NameMatcher} {@code nameMatcher}
     * for this binder
     * @param type - the parameterized type of this binder.
     * @param nameMatcher - the {@code NameMatcher} for this binder
     * @param binders - optional nested binders.
     */
    public ResultSetBinder(Class<T> type, NameMatcher nameMatcher, ResultSetBinder<?>... binders) {
        this(type, binders);
        this.nameMatcher = nameMatcher;

    }

     /**
     * Sets the default {@code NameMatcher} that will be used by all {@code ResultSetBinder}s.
     * To set {@code NameMatcher} per instance use
      * method {@link ResultSetBinder#setNameMatcher(NameMatcher) setNameMatcher};
     * @param nameMatcher - the <code>NameMatcher</code>
     */
    public static void setDefaultMatcher(NameMatcher nameMatcher) {
        defaultMatcher = nameMatcher;
    }


    /**
     * Sets the name matcher for this {@code ResultSetBinder}.
     * @param nameMatcher the {@code NameMatcher}.
     */
    public synchronized void setNameMatcher(NameMatcher nameMatcher) {
        this.nameMatcher = nameMatcher;
    }

    /**
     * Behaves just like the above method, but also, if {@code useForNested} is {@code true}
     * uses the passed {@code nameMatcher} for all nested {@code ResultSetBinders}.
     * @param nameMatcher the {@code NameMatcher}
     * @param useForNested  flag
     */
    public synchronized void setNameMatcher(NameMatcher nameMatcher, boolean useForNested) {
        this.nameMatcher = nameMatcher;
        if (useForNested) {
            for (ResultSetBinder binder : resultSetBinders) {
                binder.setNameMatcher(nameMatcher, true);
            }
        }
    }




    /**
     * Binds a single row of the {@code ResultSet}  to an instance of T.
     * The method will not call {@code next} on the {@code ResultSet}
     * @param resultSet - the <code>ResultSet</code>
     * @return  - instance of T populated with the values from <code>resultSet</code>
     * @throws SQLException
     */
    public T bindRow(ResultSet resultSet) throws SQLException {
        if (creator == null) {
            processResultSetMetaData(resultSet.getMetaData());
        }

        return bindRowInternal(resultSet);
    }



    private T bindRowInternal(ResultSet resultSet) throws SQLException {
            T t;
            try {
                t = creator.create(resultSet);

                try {
                    for (ColumnBinder binder : columnBinders) {
                        binder.bindColumn(resultSet, t);
                    }
                    for (Map.Entry<Method, ResultSetBinder<?>> entry : nestedBindersMap.entrySet()) {
                        Object value = entry.getValue().bindRowInternal(resultSet);
                        entry.getKey().invoke(t, value);
                    }
                } catch (IllegalAccessException e){
                    throw new SQLException(e.getMessage());
                } catch (InvocationTargetException e) {
                    throw new SQLException(e.getMessage());
                }
            } catch (SQLException e) {
                throw e;
            }

            return t;
        }



    /**
     * Binds the {@code ResultSet}  to the collection of T {@code tColl}
     * and returns the populated collection.
     * @param resultSet - the <code>ResultSet</code>
     * @param tColl - Collection of parameterized type T
     * @return - collection populated from the result set.
     * @throws SQLException
     */
    public Collection<T> bindResultSet(ResultSet resultSet, Collection<T> tColl) throws SQLException {
        if (creator == null)
            processResultSetMetaData(resultSet.getMetaData());
        T t;
        while (resultSet.next()) {
            t = bindRowInternal(resultSet);
            tColl.add(t);
        }
        return tColl;

    }

    /**
     * Binds the {@code ResultSet}  to a List of T.
     * Uses ArrayList implementaton of List.
     * @param resultSet  the {@code ResultSet}
     * @return the populated List of T. If the {@code ResultSet} contains
     * no rows method returns emtpy list.
     * @throws SQLException
     */
    public List<T> bindResultSet(ResultSet resultSet) throws SQLException {
        ArrayList<T> tList = new ArrayList<T>(lastListSize);
         if (creator == null)
            processResultSetMetaData(resultSet.getMetaData());
        T t;
        while (resultSet.next()) {
            t = bindRowInternal(resultSet);
            tList.add(t);
        }
        return tList;
    }


    /**
     * Returns the parameterized type of this {@code ResultSetBinder}
     * @return - parameterized type.
     */
    public Class<T> getParameterizedType() {
        return type;
    }


    private synchronized void processResultSetMetaData(ResultSetMetaData metaData)
            throws SQLException {
        if (creator != null) {
            return;
        }

        makeCreator(metaData);

        int columnCount = metaData.getColumnCount();
        ColumnBinder[] binders = new ColumnBinder[columnCount];
        Method []methods = type.getMethods();
        Set<Method> usedMethods = new HashSet<Method>();
        for (int col = 1; col <= metaData.getColumnCount(); col++) {
            if (mappedColIndeces.contains(col)) { continue; }

            for (Method method : methods) {
                if (usedMethods.contains(method)) { continue; }

                if (nameMatcher.matches(type.getSimpleName(), metaData.getTableName(col),
                        method.getName(), metaData.getColumnName(col))) {
                    Class<?>[] parameterTypes = method.getParameterTypes();
                    if (parameterTypes.length == 1) {
                        Class<?> paramType = parameterTypes[0];
                        if (paramType.equals(String.class)) {
                            binders[col - 1] = new ColumnBinder(new StringGetter(col), method);
                        } else if (paramType.equals(int.class) || paramType.equals(Integer.class)) {
                            binders[col - 1] = new ColumnBinder(new IntegerGetter(col), method);
                        } else if (paramType.equals(double.class) || paramType.equals(Double.class)) {
                            binders[col - 1] = new ColumnBinder(new DoubleGetter(col), method);
                        } else if (paramType.equals(boolean.class) || paramType.equals(Boolean.class)) {
                            binders[col - 1] = new ColumnBinder(new BooleanGetter(col), method);
                        } else if (paramType.equals(long.class) || paramType.equals(Long.class)) {
                            binders[col - 1] = new ColumnBinder(new LongGetter(col), method);
                        } else if (paramType.equals(float.class) || paramType.equals(Float.class)) {
                            binders[col - 1] = new ColumnBinder(new FloatGetter(col), method);
                        } else if (paramType.equals(short.class) || paramType.equals(Short.class)) {
                            binders[col - 1] = new ColumnBinder(new ShortGetter(col), method);
                        } else if (paramType.equals(byte.class) || paramType.equals(Byte.class)) {
                            binders[col - 1] = new ColumnBinder(new ByteGetter(col), method);
                        } else if (paramType.equals(Timestamp.class)) {
					        binders[col - 1] = new ColumnBinder(new TimestampGetter(col), method);
				        } else if (paramType.equals(Time.class)) {
					        binders[col - 1] = new ColumnBinder(new TimeGetter(col), method);
				        } else if (paramType.equals(java.sql.Date.class)) {
					        binders[col - 1] = new ColumnBinder(new DateGetter(col), method);
                        } else if (paramType.isAssignableFrom(java.util.Date.class)) {
                            String colClassName = metaData.getColumnClassName(col);
                            if (colClassName.equals("java.sql.Date")) {
                                binders[col - 1] = new ColumnBinder(new DateGetter(col), method);
                            } else if (colClassName.equals("java.sql.Time"))  {
                                binders[col - 1] = new ColumnBinder(new TimeGetter(col), method);
                            } else if (colClassName.equals("java.sql.Timestamp")) {
                                binders[col - 1] = new ColumnBinder(new TimestampGetter(col), method);
                            }
                        }
                        else if (paramType.equals(BigDecimal.class))
                            binders[col - 1] = new ColumnBinder(new BigDecimalGetter(col), method);
                    }

                    if (binders[col - 1] != null) {
                         mappedColIndeces.add(col);

                        if (debug) {
                            debugMessages.add(new DebugMessage(COLUMN_TO_METHOD,
                                    col, metaData.getColumnName(col),
                                    metaData.getColumnTypeName(col),
                                    binders[col - 1].getMethod()));
                        }
                        usedMethods.add(method);
                        break;
                    }
                }
            }///~ End for
        }

        for (ColumnBinder binder : binders) {
            if (binder != null) {
                columnBinders.add(binder);
            }
        }

        for (ResultSetBinder<?> binder : resultSetBinders) {
            binder.mappedColIndeces.clear();
            for (Method method : methods) {
                Class<?> []parameterTypes = method.getParameterTypes();
                if (parameterTypes.length == 1) {
                    if (binder.getParameterizedType().equals(parameterTypes[0])) {
                        nestedBindersMap.put(method, binder);
                        binder.mappedColIndeces.addAll(this.mappedColIndeces);
                        binder.processResultSetMetaData(metaData);
                        this.mappedColIndeces.addAll(binder.mappedColIndeces);
                    }
                }
            }
        }

        if (root && warn) {
            for (int col = 1; col <= metaData.getColumnCount(); col++) {
                boolean columnNotBound = !mappedColIndeces.contains(col);
                if (columnNotBound) {
                    warnMessages.add(new DebugMessage(COLUMN_NOT_BOUND,
                            col, metaData.getColumnName(col), metaData.getColumnTypeName(col)));
                }
            }
        }

        displayLog();
    }



    private void makeCreator(ResultSetMetaData metaData) throws SQLException {

        ConstructorAnalizer<T> analizer = new ConstructorAnalizer<T>(getParameterizedType());
        ConstructorInfo<T> constructorInfo = analizer.getParameterNames();
        if (constructorInfo == null) {
            creator = new NoArgsInstanceCreator<T>(type);
            return;
        }

        Class<?>[] paramTypes = constructorInfo.constructor.getParameterTypes();
        List<String> paramNames = constructorInfo.parameterNames;
        Set<Integer> tempMappedColIndeces = new TreeSet<Integer>(mappedColIndeces);
        List<DebugMessage> tempDebugMessages = null;
        if (debug) {
            tempDebugMessages = new ArrayList<DebugMessage>();
        }
        ColumnValueGetter[] getters = new ColumnValueGetter[paramTypes.length];
        for (int col = 1; col <= metaData.getColumnCount(); col++) {
            if (tempMappedColIndeces.contains(col)) { continue; }

            for (int paramIndex = 0; paramIndex < paramTypes.length; paramIndex++) {
                if (nameMatcher.matches(
                        type.getSimpleName(), metaData.getTableName(col),
                        "set" + paramNames.get(paramIndex), metaData.getColumnName(col))) {
                    Class<?> paramType = paramTypes[paramIndex];
                    if (paramType.equals(String.class)) {
                        getters[paramIndex] = new StringGetter(col);
                    } else if (paramType.equals(int.class) || paramType.equals(Integer.class)) {
                        getters[paramIndex] = new IntegerGetter(col);
                    } else if (paramType.equals(double.class) || paramType.equals(Double.class)) {
                        getters[paramIndex] = new DoubleGetter(col);
                    } else if (paramType.equals(boolean.class) || paramType.equals(Boolean.class)) {
                        getters[paramIndex] = new BooleanGetter(col);
                    } else if (paramType.equals(long.class) || paramType.equals(Long.class)) {
                        getters[paramIndex] = new LongGetter(col);
                    } else if (paramType.equals(float.class) || paramType.equals(Float.class)) {
                        getters[paramIndex] = new FloatGetter(col);
                    } else if (paramType.equals(short.class) || paramType.equals(Short.class)) {
                        getters[paramIndex] = new ShortGetter(col);
                    } else if (paramType.equals(byte.class) || paramType.equals(Byte.class)) {
                        getters[paramIndex] = new ByteGetter(col);
                    } else if (paramType.equals(Timestamp.class)) {
                        getters[paramIndex] = new TimestampGetter(col);
                    } else if (paramType.equals(Time.class)) {
                        getters[paramIndex] = new TimeGetter(col);
                    } else if (paramType.equals(java.sql.Date.class)) {
                        getters[paramIndex] = new DateGetter(col);
                    } else if (paramType.isAssignableFrom(java.util.Date.class)) {
                        String colClassName = metaData.getColumnClassName(col);
                        if (colClassName.equals("java.sql.Date")) {
                            getters[paramIndex] = new DateGetter(col);
                        } else if (colClassName.equals("java.sql.Time")) {
                            getters[paramIndex] = new TimeGetter(col);
                        } else if (colClassName.equals("java.sql.Timestamp")) {
                            getters[paramIndex] = new TimestampGetter(col);
                        }
                    } else if (paramType.equals(BigDecimal.class)) {
                        getters[paramIndex] = new BigDecimalGetter(col);
                    }

                    if (getters[paramIndex] != null) {
                        tempMappedColIndeces.add(col);

                        if (debug) {
                            tempDebugMessages.add(new DebugMessage(COLUMN_TO_CONSTRUCTOR,
                                    col, metaData.getColumnName(col),
                                    metaData.getColumnTypeName(col),
                                    paramIndex + 1, getParameterizedType()));
                        }
                    }
                }
            }
        }///~ End  for

        for (ColumnValueGetter getter : getters) {
            if (getter == null) {
                creator = new NoArgsInstanceCreator<T>(type);
                return;
            }
        }
        mappedColIndeces.addAll(tempMappedColIndeces);
        if (debug) {
            debugMessages.addAll(tempDebugMessages);
        }

        creator = new WithArgsInstanceCreator<T>(constructorInfo.constructor, getters);

    }


    private void displayLog() {
        if (debug) {
            for (int i = 0; i < debugMessages.size(); i++) {
                log.debug((root && (i == debugMessages.size() - 1) && (warnMessages.size() == 0))
                            ? debugMessages.get(i).toString() + "\n\n"
                            : debugMessages.get(i));
                debugMessages.clear();
            }
        }

        if (warn) {
            for (int i = 0; i < warnMessages.size(); i++) {
                log.warn((root && (i == warnMessages.size() - 1))
                            ? warnMessages.get(i).toString() + "\n\n"
                            : warnMessages.get(i));
                warnMessages.clear();
            }
        }
    }
}
